
import json
import math
import os
import paramiko
import datetime

import requests

class GuidDescr(object):
    def __init__(self,name:str,atime:float,mtime:float,size:int):
        self.name = name
        self.mtime = mtime
        self.atime = atime
        self.size = size
    @property
    def mtime_dt(self):
        return datetime.datetime.fromtimestamp(self.mtime)
    
    def guid_is_changed(self,input_guid)-> tuple[bool,bool]:
        """Сравнение двух справочников по времени последней модификации

        Args:
            input_guid (GuidDescr): Входной справочник для сравнения

        Returns:
            tuple[bool,bool]: результат сравнения (Удаленный изменен, Локальный изменен)
        """
        try:
            ing:GuidDescr = input_guid
            delta = ing.mtime-self.mtime
            # print(f"{ing.name}: {delta} : r {self.size}: l {ing.size}")
            # if self.size!=ing.size or delta>3 : 
            #     print(f"!!! {self.size!=ing.size} : {delta>3}")
            #     return True
            # else: return False
            if delta>3: return (False,True)
            elif delta<0: return (True,False)
            else: return (False,False)
        except: raise    

class GuidsSync(object):
    """ Класс для синхронизации справочников с сетевым хранилищем """
    def __init__(self,host:str,user:str,password:str):
        """Конструктор класс (инициализация соединения по ssh)

        Args:
            host (str): хост
            user (str): пользователь
            password (str): пароль
        """

        try:
            self.rhost=host
            self.ruser=user
            self.rpass=password
            self.ssh:paramiko.SSHClient = paramiko.SSHClient()
            self.ssh.load_host_keys(os.path.expanduser(os.path.join("~", ".ssh", "known_hosts")))
            self.ssh.connect(host,username=user,password=password)
            self.sftp:paramiko.SFTPClient = self.ssh.open_sftp()
            self.local_changed:dict[str,GuidDescr] = {}
            self.remote_changed:dict[str,GuidDescr] = {}            
            self.remote_dir = None
            self.local_dir = None
        except: raise
    
    def set_remote_dir(self,dir:str):
        """ Установить директорию справочников на удаленном сервере """
        self.remote_dir=dir
    
    def set_local_dir(self,dir:str):
        """ Установить локальную директорию справочников """
        self.local_dir=dir
            
        
    def close_connection(self):
        """ Закрытие соединения """
        try:
            self.sftp.close()
            self.ssh.close()
        except: raise
    
    def get_remote_files(self,dir:str)->list[GuidDescr]:
        try:
            lst=[]
            for f in self.sftp.listdir(dir):
                itemName=f"{dir}/{f}"
                stat = self.sftp.stat(itemName)
                lst.append(GuidDescr(f,stat.st_atime,stat.st_mtime,stat.st_size))
            return lst 
        except: raise
    
    def get_local_files(self,dir:str)->list[GuidDescr]:
        try:
            lst=[]
            for dirname, subdirs, files in os.walk(dir):
                for f in files:
                        itemName=os.path.join(dirname, f)
                        stat =os.stat(itemName)
                        lst.append(GuidDescr(f,stat.st_atime,stat.st_mtime,stat.st_size))
            return lst
        except: raise
    
    def check(self)->tuple[bool,bool]:
        """Сравнение справочников на удаленном сервере с локальными

        Raises:
            Exception: _description_

        Returns:
            tuple[bool,bool]: (Есть изменения на сервере, Есть изменения локально)
        """
        try:
            if self.remote_dir is None or self.local_dir is None: 
                raise Exception("Не указаны пути к удаленным или локальным справочника")
            result = self.compare_files(self.remote_dir,self.local_dir)
            return (bool(result[0]),bool(result[1]))
        except: raise
        
    def compare_files(self,remote_dir:str,local_dir:str)->tuple[dict[str,GuidDescr],dict[str,GuidDescr]]:
        try:
            rlist = self.get_remote_files(remote_dir)
            llist = self.get_local_files(local_dir)
            rdict = {}
            ldict = {}
            for g in rlist: rdict[g.name]=g
            for g in llist: ldict[g.name]=g
            for key in ldict.keys():
               rguid:GuidDescr = rdict.get(key,None)
               if rguid is not None:
                   changed = rguid.guid_is_changed(ldict[key])
                #    print(f"{key} [{changed}]")
                   if changed[0]: self.remote_changed[f"{remote_dir}/{key}"] = rdict[key]                   
                   if changed[1]: self.local_changed[f"{local_dir}\\{key}"] = ldict[key]
            self.remote_dir=remote_dir
            self.local_dir=local_dir
            return (self.remote_changed,self.local_changed)
        except: raise
    
    def send_to_remote(self):
        try:
            if self.remote_dir is not None:
                for local_file in self.local_changed.keys():
                    times = (self.local_changed[local_file].atime,self.local_changed[local_file].mtime)
                    remote_file = f"{self.remote_dir}/{self.local_changed[local_file].name}"
                    self.sftp.put(local_file,remote_file)
                    self.sftp.utime(remote_file,times)
                    print(f"{self.local_changed[local_file].name} [sended]")
        except: raise
        
    def get_from_remote(self):
        try:
            if self.local_dir is not None:
                for remote_file in self.remote_changed.keys():
                    local_file = f"{self.local_dir}\\{self.remote_changed[remote_file].name}"
                    self.sftp.get(remote_file,local_file)
                    print(f"{self.remote_changed[remote_file].name} [accepted]")                    
        except: raise
        
    def reconnect(self):
        try:
            self.sftp.close()
            self.ssh.close()
            self.ssh.load_host_keys(os.path.expanduser(os.path.join("~", ".ssh", "known_hosts")))
            self.ssh.connect(hostname=self.rhost,username=self.ruser,password=self.rpass)
            self.sftp = self.ssh.open_sftp()
            self.local_changed:dict[str,GuidDescr] = {}
            self.remote_changed:dict[str,GuidDescr] = {}
        except: raise
        
class ServerList(object):
    """  Список файлов на сервере (nginx + json)  """
    def __init__(self,data:bytes) -> None:
        try:
            self.bytes_data = data
            tmp_str = self.bytes_data.decode("utf-8")
            self.json_data = json.loads(tmp_str)
            self.files = {}
            for item in self.json_data:
                dt = datetime.datetime.strptime(item["mtime"],"%a, %d %b %Y %H:%M:%S %Z")+datetime.timedelta(hours=3)
                self.files[item["name"]]=(dt.timestamp(),item["size"])
            self.is_empty = not bool(self.files)
        except: raise
        
class LocalList(object):
    """ Локальный список файлов """
    def __init__(self,dir:str,filter:str = ".xlsx") -> None:
        try:
            self.dir_list = [fi for fi in os.listdir(dir) if "~$" not in fi and filter in fi]            
            self.files={}
            for file in self.dir_list:
                fullName= f"{dir}\\{file}"
                stat = os.stat(fullName)
                self.files[file]=(stat.st_mtime,stat.st_size)
            self.is_empty = not bool(self.files)
        except: raise
        
class Synchronizer(object):
    """ Синхронизатор файлов """
    def __init__(self,repo_url:str,local_dir:str,file_filter:str=".xlsx") -> None:
        """_summary_

        Args:
            repo_url (str): URL удаленного репозитория
            local_dir (str): Локальная папка
            file_filter (str, optional): Фильтр локальных файлов. Defaults to ".xlsx".
        """
        try:
            self.repo = repo_url
            self.local_dir = local_dir
            self.server_files_empty = True
            try:
                r = requests.get(self.repo)
                self.server_files = ServerList(r.content)
                self.server_files_empty = self.server_files.is_empty
            except: raise Exception("Bad repo")
            self.local_files  = LocalList(self.local_dir,filter=file_filter)
            self.local_files_empty = self.local_files.is_empty
            self.newer_server = {}
            self.newer_local  = {}
            self.newer_server_exist = False
            self.newer_local_exist = False
            if not self.local_files_empty and not self.server_files_empty:
                for key in self.local_files.files.keys():
                    if (self.local_files.files[key][0]-self.server_files.files[key][0]) > 1:
                        self.newer_local[key]=self.local_files.files[key]
                        self.newer_local_exist = True
                    elif (self.local_files.files[key][0]-self.server_files.files[key][0]) < -1: 
                        self.newer_server[key]=self.server_files.files[key]
                        self.newer_server_exist = True
        except: raise
        
    def sync_local(self):
        """ Синхронизировать локальные файлы на сервер """
        try:
            if self.newer_local_exist:
                for key in self.newer_local.keys():
                    fullName=f"{self.local_dir}\\{key}"
                    stat  = os.stat(fullName)
                    with open(fullName,"rb") as datafile:
                        r = requests.put(f"{self.repo}{key}",data=datafile)
                        dt= math.trunc(datetime.datetime.now().timestamp())
                        datafile.close()
                        os.utime(fullName,(stat.st_atime,dt))
        except: raise
        
    def sync_server(self):
        """ Синхронизировать серверные файлы на локальную машину """
        try:
            if self.newer_server_exist:
                for key in self.newer_server.keys():
                    fullName=f"{self.local_dir}\\{key}"
                    stat  = os.stat(fullName)
                    url = f"{self.repo}{key}"
                    r = requests.get(url)
                    open(fullName,"wb").write(r.content)
                    os.utime(fullName,(stat.st_atime,self.newer_server[key][0]))
        except: raise
        
    def sync_all(self):
        """ Синхронизировать все файлы """
        try:
            self.sync_local()
            self.sync_server()
        except: raise
        
    def init_local(self):
        """ Загрузить файлы с удаленного хоста в локальную папку """
        try:
            for key in self.server_files.files.keys():
                fullName=f"{self.local_dir}\\{key}"
                url = f"{self.repo}{key}"
                r = requests.get(url)
                open(fullName,"wb").write(r.content)
                os.utime(fullName,(self.server_files.files[key][0],self.server_files.files[key][0]))
                print(f"'{key}' downloaded {self.server_files.files[key][1]} bytes")               
        except: raise
        
    def init_server(self):
        """ Загрузить файлы на удаленный хост из локального хранилища """
        try:
            for key in self.local_files.files.keys():
                fullName=f"{self.local_dir}\\{key}"
                stat  = os.stat(fullName)
                url = f"{self.repo}{key}"
                with open(fullName,"rb") as datafile:
                        r = requests.put(url,data=datafile)
                        dt= math.trunc(datetime.datetime.now().timestamp())
                        datafile.close()
                        os.utime(fullName,(stat.st_atime,dt))
                print(f"'{key}' uploaded {self.local_files.files[key][1]} bytes")
        except: raise    
        
    